﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

using SpectationClient.Stuff;

namespace SpectationClient.SpectralTools {
    
    public static class ASD_Reader {
       

        public struct GPSData {
            public bool hasData;
            public double true_heading;
            public double speed;
            public double latDecDegree;
            public double longDecDegree;
            public double altitude;
        }



        #region Consistency Checks
        public static bool isValidASDSpectrum(byte[] spec) {
            return (spec.Length > 3 && 
                            (spec[0] == 65 && //A
                             spec[1] == 83 && //S
                             spec[2] == 68)   //ColumnValues
                       );
        }

        public static bool isValidASDSpectrum(String filePath) {
            return isValidASDSpectrum(new FileInfo(filePath));
        }

        public static bool isValidASDSpectrum(FileInfo fileInfo) {
            int minimumSize = 431;
            if(!fileInfo.Exists || fileInfo.Length < minimumSize) return false;
             
            FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
            byte[] bytes = new byte[minimumSize]; //old format
            stream.Read(bytes, 0, bytes.Length);
            return isValidASDSpectrum(bytes);
        }
        #endregion

        #region Reading Functions
        public static List<Spectrum> readSpectra(List<FileInfo> asdFiles) {
            List<Spectrum> spectra = new List<Spectrum>();
            foreach(FileInfo fileInfo in asdFiles) spectra.Add(ASD_Reader.readSpectrum(fileInfo));
            return spectra;
        }

        public static Spectrum readSpectrum(FileInfo asdFile) {
            if(!ASD_Reader.isValidASDSpectrum(asdFile)) 
                throw new Exception(
                    String.Format("File {0} is not a valid asd spectrum file", asdFile.FullName));
            FileStream stream = new FileStream(asdFile.FullName, FileMode.Open, FileAccess.Read);
            
            byte[] bytes = new byte[asdFile.Length]; //old format
            stream.Read(bytes, 0, bytes.Length);
            return SpectrumFromBLOB(bytes, asdFile.Name, asdFile.Name);
        }

        public static Spectrum readSpectrum(byte[] blob, String name) {
            return ASD_Reader.readSpectrum(blob, name, null);
        }
        public static Spectrum readSpectrum(byte[] blob, String name, String filename) {
            if(!ASD_Reader.isValidASDSpectrum(blob)) throw new Exception("BLOB is not a valid asd spectrum");
            return SpectrumFromBLOB(blob, name, filename);
        }
        #endregion

        /// <summary>
        /// This function returns a spectrum from the information given in the ASD binary file
        /// </summary>
        /// <param name="blob"></param>
        /// <param name="name"></param>
        /// <param name="filename"></param>
        /// <returns></returns>
        private static Spectrum SpectrumFromBLOB(byte[] blob, String name, String filename) {
            Spectrum spec = new Spectrum(name);
            spec.Bands = ASD_Reader.getX_Values(blob);
            //spec.BandValues = DataHelper.ConvertToNullable(ASD_Reader.getY_Values(blob));
            spec.BandValues = ASD_Reader.getY_Values(blob);
            spec.Comments = ASD_Reader.getComments(blob);
            spec.DataType = ASD_Reader.getDataType(blob);
            spec.FileName = filename;
            spec.FileType = Spectrum.SpectrumFileType.ASD;
            spec.IntrumentType = ASD_Reader.getInstrumentType(blob);
            spec.SensorCalibrationNumber = ASD_Reader.getCalibrationNo(blob);
            spec.SensorID = ASD_Reader.getInstrumentNo(blob);
            spec.SpecType = ASD_Reader.getSpectrumType(blob);
            spec.TimeSaved = ASD_Reader.getTimeSpectrumSaved(blob);
            spec.originalASD_BLOB = blob;
            return spec;
        }

        public static int getForeOptic(byte[] by){
            return BitConverter.ToInt16(by, 394);
        }



        /// <summary>
        /// Returns the data type: C/.Net - ASD Description.
        /// Float / Single  : 0
        /// Integer / Int16 : 1
        /// Double / Double : 2
        /// unknown / null  : 3
        /// </summary>
        /// <param name="by"></param>
        /// <returns></returns>
        private static Type getDataType(byte[] by) {
            const byte FLOAT_FORMAT = 0;
            const byte INTEGER_FORMAT = 1;
            const byte DOUBLE_FORMAT = 2;
            const byte UNKNOWN_FORMAT = 3;

                switch (by[199]) {
                    case FLOAT_FORMAT: return typeof(Single);
                    case INTEGER_FORMAT: return typeof(Int16);
                    case DOUBLE_FORMAT: return typeof(Double);
                    case UNKNOWN_FORMAT: return null;
                }
            return null;
        }

        public static String getComments(byte[] by) {
            ASCIIEncoding asc = new ASCIIEncoding();
                return asc.GetString(by, 3, 150).Trim();
        }

        public static Spectrum.InstrumentType getInstrumentType(byte[] by) {
            switch (by[431]) {
                case 1: return Spectrum.InstrumentType.PSII;   //"PSII"; break;
                case 2: return Spectrum.InstrumentType.LSVNIR; //"LSVNIR"; break;
                case 3: return Spectrum.InstrumentType.FSVNIR; //"FSVNIR"; break;
                case 4: return Spectrum.InstrumentType.FSFR;   //"FSFR"; break;
                case 5: return Spectrum.InstrumentType.FSNIR;  //"FSNIR"; break;
                case 6: return Spectrum.InstrumentType.CHEMT;  //"CHEMT"; break;
                case 7: return Spectrum.InstrumentType.FSFR_UNATTENDED; //"FSFR_UNATTENDED_INSTRUMENT"; break;
                case 0: return Spectrum.InstrumentType.UNKNOWN; //"UNKNOWN_INSTRUMENT"; break;
                default: return Spectrum.InstrumentType.UNKNOWN;
            }
//            return SPECLIB_Spectrum.InstrumentType.UNKNOWN;
        }

        public static String getInstrumentTypeString(byte[] by) {
            Spectrum.InstrumentType type = ASD_Reader.getInstrumentType(by);
            return Enum.GetName(typeof(Spectrum.InstrumentType), type);
        
        }
        /// <summary>
        /// ASD Sensor Number
        /// </summary>
        /// <param name="by"></param>
        /// <returns></returns>
        public static int getInstrumentNo(byte[] by) {
            return BitConverter.ToUInt16(by, 400);
        }

        /// <summary>
        /// GPS Data Struct
        /// </summary>
        /// <param name="ba"></param>
        /// <returns></returns>
        private static GPSData getGPSData(byte[] spec) {
            GPSData gps = new GPSData();
            gps.true_heading = BitConverter.ToDouble(spec, 334);
            gps.speed = BitConverter.ToDouble(spec, 342);
            gps.latDecDegree = nmea2DecimalDegrees(BitConverter.ToDouble(spec, 350));
            gps.longDecDegree = -1 * nmea2DecimalDegrees(BitConverter.ToDouble(spec, 358));
            gps.altitude = BitConverter.ToDouble(spec, 366);
            gps.hasData = gps.true_heading != 0 ||
                          gps.speed != 0 ||
                          gps.latDecDegree != 0 ||
                          gps.longDecDegree != 0 ||
                          gps.altitude != 0;
            return gps;
        }

        //Gibt Datentyp-Kuerzel zurück 
        private static Spectrum.SpectrumType getSpectrumType( byte[] spec) {
            switch (spec[186]) {
                case 0: return Spectrum.SpectrumType.RAW_TYPE;       //type = "RAW"; break;
                case 1: return Spectrum.SpectrumType.REF_TYPE;       //type = "REF"; break;
                case 2: return Spectrum.SpectrumType.RAD_TYPE;       //type = "RAD"; break;
                case 3: return Spectrum.SpectrumType.NOUNITS_TYPE;   //type = "NOUNITS"; break;
                case 4: return Spectrum.SpectrumType.IRRAD_TYPE;     //type = "IRRAD"; break;
                case 5: return Spectrum.SpectrumType.QI_TYPE;        //type = "QI"; break;
                case 6: return Spectrum.SpectrumType.TRANS_TYPE;     //type = "TRANS"; break;
                case 7: return Spectrum.SpectrumType.UNKNOWN_TYPE;   //type = "UNKNOWN"; break;
                case 8: return Spectrum.SpectrumType.ABS_TYPE;       //type = "ABS"; break;
            }

            return Spectrum.SpectrumType.UNKNOWN_TYPE;
        }

      
        /// <summary>
        /// Returns the Number of DC (Dark Current) Measurements for average DC spectrum
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        private static int getDC_Count(byte[] spec) {
            return BitConverter.ToInt16(spec, 425);
        }

        /// <summary>
        /// Return the number of averaged single spectra that were used to create this spectrum 
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        private static int getRef_count(byte[] spec) {
            return BitConverter.ToInt16(spec, 427);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        private static int getASDsample_count(byte[] spec) {
            return BitConverter.ToInt16(spec, 429);
        }

        public static Single[] getX_Values(byte[] spec) {
            int channels = getChannelCount(spec);
            Single firstWaveL = getFirstWaveLength(spec);
            Single waveLengthStep = getWaveLengthStep(spec);
            Single[] dv = new Single[channels];
            for (int i = 0; i < channels; i++) {
                dv[i] = firstWaveL + i * waveLengthStep;
            }
            return dv;
        }

        /// <summary>
        /// Return the spectral values
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        public static Single[] getY_Values(byte[] spec) {
            int channels = getChannelCount( spec);
            Single[] dv = new Single[channels];
            int typeLength = (int)(spec.Length - 484) / channels;

            if (typeLength == 4) {
                for (int i = 0; i < channels; i++) {
                    dv[i] = Convert.ToSingle(BitConverter.ToSingle(spec, 484 + i * 4));
                }
            } else if (typeLength == 2) {
                for (int i = 0; i < channels; i++) { 
                    dv[i] = Convert.ToSingle(BitConverter.ToInt16(spec, 484 + i * 2));
                }
            }
            
            return dv;
        }

        /// <summary>
        /// Returns the time the last Dark Current was measurend
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        public static long getASDdc_time(byte[] spec) {
            return  BitConverter.ToInt64(spec, 187); 
        }

        /// <summary>
        /// Returns the Date and Time the spectrum was recorded.
        /// 160 int   tm_sec;		// seconds [0,61]
        /// 162 int   tm_min;		// minutes [0,59]
        /// 164 int   tm_hour;		// hour [0,23]
        /// 166 int   tm_mday;		// day of month [1,31]
        /// 168 int   tm_mon;		// month of year [0,11]
        /// 170 int   tm_year;		// years since 1900
        /// 172 int   tm_wday;		// day of week [0,6] (Sunday = 0)
        /// 174 int   tm_yday;		// day of year [0,365]
        /// 176 int   tm_isdst;		// daylight savings flag
        /// 
        /// </summary>
        /// <param name="ba"></param>
        /// <returns></returns>
        public static DateTime getTimeSpectrumSaved(byte[] spec) {
            DateTime dt = new DateTime();
            
                int sec = BitConverter.ToUInt16(spec, 160);//[0,61]
                int min = BitConverter.ToUInt16(spec, 162);//[0,59]
                int hour = BitConverter.ToUInt16(spec, 164);//[0,23]
                int mday = BitConverter.ToUInt16(spec, 166);//[1,31]
                int mon = BitConverter.ToUInt16(spec, 168) + 1;//[0,11] +1 wegen C# [1,12]
                int year = 1900 + BitConverter.ToUInt16(spec, 170);//Jahre seit 1900
                int wday = BitConverter.ToUInt16(spec, 172);
                int yday = BitConverter.ToUInt16(spec, 174);
                int isdsd = BitConverter.ToUInt16(spec, 176);
                try {
                    dt = new DateTime(year, mon, mday, hour, min, sec);
                } catch { }
            
            return dt;
        }


        public static DateTime getTimeDarcCurrentCorrection(byte[] spec) {
            return new DateTime(BitConverter.ToUInt32(spec, 182));
        }

        public static DateTime getTimeWhiteReference(byte[] spec) {
            return new DateTime(BitConverter.ToUInt32(spec, 187));
        }


        /*
                public tm time(byte[] ba) {
                    tm time = new tm();
                    if(validSpectrum(ref ba)){
                        time.tm_sec  = Convert.ToInt16(ba[160]);//[0,61]
                        time.tm_min  = Convert.ToInt16(ba[162]);//[0,59]
                        time.tm_hour = Convert.ToInt16(ba[164]);//[0,23]
                        time.tm_mday = Convert.ToInt16(ba[166]);//[1,31]
                        time.tm_mon  = Convert.ToInt16(ba[168]);//[0,11]
                        time.tm_year = 1900 + Convert.ToInt16(ba[170]);//Jahre seit 1900
                        time.tm_wday = Convert.ToInt16(ba[172]);
                        time.tm_yday = Convert.ToInt16(ba[174]);
                        time.tm_isdsd= Convert.ToInt16(ba[176]);
                    }
                    return time;
                }
                */
        //Liefert die Werte eines Spektrums als Byte[]
        //Transformiert auf den Bereich 0-255
        /*
        public byte[] getNormalizedValues(byte[] ba, int minY, int maxY) {
            Single[] values = null;
            if (validSpectrum(ref ba)) {
                //Abspaltung des Data-Teils
                int headerOffset = 484;//Offset = erstes Byte des Datenteils
                byte[] data = new byte[ba.Length - headerOffset];
                for (int i = 0; i < data.Length; i++) {
                    data[i] = ba[i + headerOffset];
                }

                HdrInfo info = HeaderValues(ref ba);
                values = new Single[info.channels];
                //DPType = float
                if (info.DPType == 4) {
                    for (int i = 0; i < values.Length; i++) {
                        values[i] = BitConverter.ToSingle(data, 4 * i);
                    }

                    //DPType = integer
                } else if (info.DPType == 2) {
                    for (int i = 0; i < values.Length; i++) {
                        values[i] = BitConverter.ToInt16(data, 2 * i);
                    }
                }

                //Single-Werte zu Byte machen

            }
            return null;
        }
        */

        /// <summary>
        /// Returns a single spectrum values
        /// </summary>
        /// <param name="ba"></param>
        /// <param name="x">zero based value iPosition</param>
        /// <returns></returns>
        private static Single getSingleValue(byte[] spec, int x) {
            return spec[483 + x * 4];
        }

        /// <summary>
        /// Returns all spectral values as double values
        /// </summary>
        /// <param name="ba"></param>
        /// <returns></returns>
        public static double[] getValueArray(byte[] spec) {
            double[] d = new double[getChannelCount(spec)];
            for (int i = 0; i < d.Length; i++) {
                d[i] = getChannelValue( spec, i);
            }
            return d;
        }

        //Gibt alle Werte als byte[] Array wieder, in dem sich Double-Werte befinden
        public static byte[] getValueByteArray(byte[] spec) {
            //1. Anzahl der Werte
            int ch = getChannelCount(spec);
            byte[] b = new byte[ch * 8]; //*8, da ein Double aus 8 byte besteht
            for (int c = 1; c <= ch; c++) {
                byte[] v = BitConverter.GetBytes(getChannelValue( spec, c));
                //double d = BitConverter.ToDouble(v,0);
                v.CopyTo(b, (c - 1) * 8);
            }
            return b;
        }

        public static Int32 getChannelCount( byte[] spec) {
         
                return BitConverter.ToUInt16(spec, 204);
         
        }


        
        /// <summary>
        /// Returns a single spectral value from a certain band.
        /// </summary>
        /// <param name="ba">Byte array of total ASD file</param>
        /// <param name="channelIndex">Zero based iPosition of channel / band</param>
        /// <returns></returns>
        public static Single getChannelValue(byte[] ba, int channelIndex) {
            //HeaderOffset
            int typeLength = (int)(ba.Length - 484) / 2151; //2151 Channels
            switch (typeLength){
                case 2 : return Convert.ToSingle(BitConverter.ToInt16(ba, 484 + channelIndex * 2));
                case 4 : return Convert.ToSingle(BitConverter.ToSingle(ba, 484 + channelIndex * 4));
                default : throw new Exception("Unknown typeLength");
            }
        }

        // Channel started bei NUll!!!
        //Es wird generell Double ausgegeben
        /*
        public String getChannelValueString(byte[] ba, int channel) {
            //HeaderOffset
            float typeLength = -1;
            int ch = channel - 1;
            if (getFileExtension(ba) == FILETYPE_ASD) {

                //Header-Offset ist 484 -> an der 485. Stelle beginnt der Datenteil
                typeLength = (float)(ba.Length - 484) / 2151; //2151 Channels
                try {
                    if (typeLength == 4) return Convert.ToString(Convert.ToDouble(BitConverter.ToSingle(ba, 484 + ch * 4)));
                    if (typeLength == 2) return Convert.ToString(Convert.ToDouble(BitConverter.ToInt16(ba, 484 + ch * 2)));
                } catch (Exception ex){
                    String s = ex.Message;
                }
            }

            return "ERROR in ASD_Reader.getChannelValueString";
        }
        */

        public static Single getFirstWaveLength( byte[] spec) {
                return BitConverter.ToSingle(spec, 191);
        }

        public static Single getLastWaveLength( byte[] spec) {
            Single first = BitConverter.ToSingle(spec, 191);
            Single step = getWaveLengthStep(spec);
            int cnt = getChannelCount( spec);
            return first + step * ((Single)cnt - 1);
        }


        //Gibt den Datenteil der Spektraldatei als byte-Array zurück.
        /*
        public byte[] getData(byte[] ba) {
            byte[] imageBLOB = null;
            if(validSpectrum(ref ba)){
                int Type = getSpecType(ba);
                if(Type == ASD){
                    HdrInfo info = HeaderValues(ref ba);
                    int headerOffset = 484;

                    imageBLOB = new byte[ba.Length - headerOffset];
                    
                    for (int i = 0; i < imageBLOB.Length; i++) {
                        imageBLOB[i] = ba[i + headerOffset];
                    }
                }
                //Hier koennen Spezifikationen für andere Dateitypen erfolgen
            }else{
                imageBLOB = null;
            }

            return imageBLOB;
        }
        */

        public static Single getWaveLengthStep(byte[] spec) {
            
                return BitConverter.ToSingle(spec, 195);
            
        }

        //Liefert Infos aus dem Header der Spektraldatei
        /*
        public HdrInfo HeaderValues(byte[] header){
            HdrInfo info = new HdrInfo();
            if(validSpectrum(ref header)){
              //Ermittlung benötigter HeaderDaten
                //byte b1 = header[0];
                
                for (int i = 0; i < 3; i++) {
                    Char cDBColumnName = (char)header[i];
                    info.company += cDBColumnName;
                }
                for (int i = 3; i <= 157; i++) {
                    Char cDBColumnName = (char)header[i];
                    info.comments += cDBColumnName;
                }
                info.comments.Trim();

                Char x = BitConverter.ToChar(header, 0); 
                info.channels = BitConverter.ToUInt16(header, 204);
                info.ch1_waveL = BitConverter.ToSingle(header, 191);
                info.waveLStep = BitConverter.ToSingle(header, 195);
                info.ymin = BitConverter.ToSingle(header, 402);
                info.ymax = BitConverter.ToSingle(header, 406);
                info.xmin = BitConverter.ToSingle(header, 410);
                info.xmax = BitConverter.ToSingle(header, 414);
                info.DataType = getSpectrumType(ref header);
                info.DataFormat = BitConverter.ToChar(header, 199); 
                info.DPType = (header.Length-484) / info.channels;//2 = integer, 4 = float
                info.instrument_number = BitConverter.ToUInt16(header, 400);
                info.instrument_type = getInstrumentType(header);
            }
            return info;
    }
        */

        /*
        public String getCompany(byte[] ba){
        if (getFileExtension(ba) == FILETYPE_ASD) {
            return "ASD";
            }

            return "UNKNOWN";
        }
        */


        public static int getCalibrationNo(byte[] spec) {
                return BitConverter.ToUInt16(spec, 398);
        }


        private static double nmea2DecimalDegrees(double nmea) {
            double deg = Math.Truncate(nmea / 100);
            double minutes = nmea - 100 * deg;
            double decdeg1 = deg + minutes / 60;

            return decdeg1;
        }


    }
}